/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package com.rultax.neural.feedforward.train.backpropagation;

import com.rultax.matrix.Matrix;
import com.rultax.matrix.MatrixMath;
import com.rultax.neural.feedforward.FeedForwardLayer;
import com.rultax.neural.util.BoundNumbers;

/**
 *
 * @author Scott
 */
public class BackpropagationLayer {
    private final double[] error;
    private final double[] errorDelta;
    private Matrix accMatrixDelta;
    private int biasRow;
    private Matrix matrixDelta;
    private final Backpropagation backpropagation;
    private final FeedForwardLayer layer;
    
    public BackpropagationLayer(final Backpropagation backpropagation,
            final FeedForwardLayer layer){
        this.backpropagation = backpropagation;
        this.layer = layer;
        
        final int neuronCount = layer.getNeuronCount();
        
        this.error = new double[neuronCount];
        this.errorDelta = new double[neuronCount];
        
        if(layer.getNext() != null){
            this.accMatrixDelta = new Matrix(layer.getNeuronCount() + 1, layer.getNext().getNeuronCount());
            this.matrixDelta = new Matrix(layer.getNeuronCount() + 1, layer.getNext().getNeuronCount());
            this.biasRow = layer.getNeuronCount();
        }
    }
    
    public void accumulateMatrixDelta(final int i1, final int i2, final double value){
        this.accMatrixDelta.add(i1, i2, value);
    }
    
    public void accumulateThresholdDelta(final int index, final double value){
        this.accMatrixDelta.add(this.biasRow, index, value);
    }
    
    public void calcError(){
        final BackpropagationLayer next = this.backpropagation.getBackpropagationLayer(this.layer.getNext());
        
        for (int i = 0; i < this.layer.getNext().getNeuronCount(); i++) {
            for (int j = 0; j < this.layer.getNeuronCount(); j++) {
                accumulateMatrixDelta(j, i, next.getErrorDelta(i) * this.layer.getFire(j));
                setError(j, getError(j) + this.layer.getMatrix().get(j, i) * next.getErrorDelta(i));
            }
            accumulateThresholdDelta(i, next.getErrorDelta(i));
        }
        if(this.layer.isHidden())
            for(int i = 0; i < this.layer.getNeuronCount(); i++)
                setErrorDelta(i, BoundNumbers.bound(calculateDelta(i)));
    }
    
    public void calcError(final double ideal[]){
        for (int i = 0; i < this.layer.getNeuronCount(); i++) {
            setError(i, ideal[i] - this.layer.getFire(i));
            setErrorDelta(i, BoundNumbers.bound(calculateDelta(i)));
        }
    }
    
    private double calculateDelta(final int i){
        return getError(i) * this.layer.getActivationFunction().derivativeFunction(this.layer.getFire(i));
    }
    
    public void clearError(){
        for (int i = 0; i < this.layer.getNeuronCount(); i++) {
            this.error[i] = 0;
        }
    }
    
    public double getError(final int i){
        return this.error[i];
    }
    
    public double getErrorDelta(final int i){
        return this.errorDelta[i];
    }
    
    public void learn(final double learnRate, final double momentum){
        if(this.layer.hasMatrix()){
            final Matrix m1 = MatrixMath.multiply(this.accMatrixDelta, learnRate);
            final Matrix m2 = MatrixMath.multiply(this.matrixDelta, momentum);
            
            this.matrixDelta = MatrixMath.add(m1, m2);
            this.layer.setMatrix(MatrixMath.add(this.layer.getMatrix(), this.matrixDelta));
            this.accMatrixDelta.clear();
        }
    }
    
    public void setError(final int index, final double e){
        this.error[index] = BoundNumbers.bound(e);
    }
    
    public void setErrorDelta(final int i, final double d){
        this.errorDelta[i] = d;
    }
}
